<?php declare(strict_types=1);

namespace application\units;

class Db
{
    
    private $sql;
    
    private $bindValue;
    
    private $transactionTimes = 0;
    
    /**
     * @var \PDO
     */
    private $pdo = null;
    
    private $table_pre = null;
    
    /**
     * Db constructor.
     *
     * @param string $dsn
     * @param string $username
     * @param string $password
     * @param array  $options
     */
    public function __construct(string $dsn, string $username, string $password, array $options)
    {
        $this->pdo = new \PDO($dsn, $username, $password, $options);
    }
    
    /**
     * @param       $sql
     * @param array $bindValue
     *
     * @return array
     */
    private function sql($sql, $bindValue = [])
    {
        $bind = [];
        foreach ($bindValue as $key => $value) {
            if (is_array($value)) {
                if ($value) {
                    $arrayKey = [];
                    foreach ($value as $index => $item) {
                        $index                     = $index . bin2hex(random_bytes(3));
                        $arrayKey[]                = ':_' . $key . $index;
                        $bind['_' . $key . $index] = $item;
                    }
                    $sql = str_replace(':' . $key, implode(',', $arrayKey), $sql);
                } else {
                    $bind[$key] = false;
                }
            } else {
                $bind[$key] = $value;
            }
        }
        $this->sql       = $sql;
        $this->bindValue = $bind;
        return [$sql, $bind];
    }
    
    /**
     * @param false $realSql
     *
     * @return string|string[]
     */
    public function getSql($realSql = false)
    {
        if (!$realSql) {
            return $this->sql;
        }
        $aKey   = [];
        $aValue = [];
        foreach ($this->bindValue as $key => $value) {
            array_push($aKey, ':' . $key);
            array_push($aValue, '"' . addslashes((string)$value) . '"');
        }
        return str_replace($aKey, $aValue, $this->sql);
    }
    
    /**
     * @param       $sql
     * @param array $bindValue
     * @param null  $mode must be one of the PDO::FETCH_* constants
     *
     * @return mixed
     */
    public function find($sql, $bindValue = [], $mode = null)
    {
        $sql .= ' limit 1';
        [$sql, $bind] = $this->sql($sql, $bindValue);
        $stat = $this->pdo->prepare($sql);
        $stat->execute($bind);
        return $stat->fetch((int)$mode);
    }
    
    /**
     * @param       $sql
     * @param array $bindValue
     * @param null  $mode must be one of the PDO::FETCH_* constants
     *
     * @return array
     */
    public function query($sql, $bindValue = [], $mode = null)
    {
        [$sql, $bind] = $this->sql($sql, $bindValue);
        $stat = $this->pdo->prepare($sql);
        $stat->execute($bind);
        return $stat->fetchAll((int)$mode);
    }
    
    /**
     * @param       $sql
     * @param array $bindValue
     *
     * @return false|\PDOStatement
     */
    public function execute($sql, $bindValue = []): \PDOStatement
    {
        [$sql, $bind] = $this->sql($sql, $bindValue);
        $stat = $this->pdo->prepare($sql);
        $stat->execute($bind);
        return $stat;
    }
    
    /**
     * @param null $name
     *
     * @return string
     */
    public function lastInsertId($name = null)
    {
        return $this->pdo->lastInsertId($name);
    }
    
    /**
     * 支持事务嵌套
     *
     * @return bool
     */
    public function beginTransaction()
    {
        $bool = $this->transactionTimes == 0 ? $this->pdo->beginTransaction() : false;
        $this->transactionTimes++;
        return $bool;
    }
    
    /**
     * 支持事务嵌套提交
     *
     * @return bool
     */
    public function commit()
    {
        $bool = $this->transactionTimes == 1 ? $this->pdo->commit() : false;
        $this->transactionTimes--;
        $this->transactionTimes = max(0, $this->transactionTimes);
        return $bool;
    }
    
    /**
     * 支持事务嵌套回滚
     *
     * @return bool
     */
    public function rollback()
    {
        $bool                   = $this->transactionTimes == 1 ? $this->pdo->rollBack() : false;
        $this->transactionTimes = max(0, $this->transactionTimes--);
        return $bool;
    }
    
    public function errorInfo()
    {
        return $this->pdo->errorInfo();
    }
    
    public function errorCode()
    {
        return $this->pdo->errorCode();
    }
}